﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Manoso.Data.FluentMap
{
    internal interface IMapTable : IMapQuery
    {
        string Name { get; set; }
        IDictionary<string, IMapTable> TypeTables { get; }
        IDictionary<string, IMapProperty> NameProperties { get; }
        IDictionary<Type, IMapTable> Derives { get; }
        IList<IMapJoin> Joins { get; }

        void Push(IJoinSelect select);
        string GetCommand(IMapTask task, IJoinSelect select);        
    }

    public interface IMapTable<TP> : IMapQuery<TP>
    {
        /// <summary>
        /// Define a table join between the current type and a type from upper level of the object hierarchy map.
        /// </summary>
        /// <typeparam name="TJ">The joining upper level object type.</typeparam>
        /// <param name="alias">Table alias for the upper level type.</param>
        /// <returns>A IMapJoin object defining the table join.</returns>
        IMapJoin<TP, TJ> Join<TJ>(string alias = null);

        /// <summary>
        /// Define name and alias for the underlining table.
        /// </summary>
        /// <param name="table">Name of the table.</param>
        /// <param name="alias">Alias of the table, it must be unique within the object hierarchy map.</param>
        void Tag(string table, string alias);
    }

    internal class MapTable<T, TP> : MapQuery<T, TP>, IMapTable<TP>, IMapTable
    {
        public string Name { get; set; }
        public IList<IMapJoin> Joins { get; private set; }
        public IDictionary<string, IMapTable> TypeTables { get; set; }
        public IDictionary<string, IMapProperty> NameProperties { get; internal set; }

        public IDictionary<Type, IMapTable> Derives { get; private set; }

        public MapTable()
        {
            Joins = new List<IMapJoin>();
            Derives = new Dictionary<Type, IMapTable>();
        }

        public void Tag(string table, string alias)
        {
            Alias = alias;
            Name = table;
        }

        public override void Setup()
        {
            base.Setup();
            if (!TypeTables.ContainsKey(Alias))
                TypeTables.Add(Alias, this);
        }

        public IMapJoin<TP, TJ> Join<TJ>(string alias = null)
        {
            var join = new MapJoin<TP, TJ>(Alias, alias);
            Joins.Add(join);
            return join;
        }

        public void Push(IJoinSelect select)
        {
            if (select.FromAlias != Alias && Joins.Count <= 0) return;

            select.Ids.Add(Alias, Keys);
            select.Properties.Add(Alias, Columns);
            select.Joins.AddMany(Joins);

            foreach (var mp in Columns)
            {
                NameProperties.Add(mp.Column.Alias, mp);
            }
        }

        public string GetCommand(IMapTask task, IJoinSelect select)
        {
            var adaptor = task.Adaptor;
            var columns = select.Properties.SelectMany(x => x.Value.Select(y => y.Column)).ToArray();
            var innerJoins = select.Joins.Select(x =>
            {
                var tableSource = TypeTables[x.SourceAlias];
                var tableTarget = TypeTables[x.TargetAlias];
                return adaptor.GetLeftJoin(tableSource.Name, tableSource.Alias, tableTarget.Alias, x.Sources.Select(y => NameProperties[y.Alias].Column.Name), x.Targets.Select(y => NameProperties[y.Alias].Column.Name));
            }).ToArray();

            return adaptor.GetSelect(select.FromTable, select.FromAlias, select.Where, columns, innerJoins);
        }

        public override object SetValue(object parent, Action<object, IEnumerable<IMapProperty>> read)
        {
            var result = base.SetValue(parent, read);
            if (result != null) return result;

            foreach (var table in Derives)
            {
                result = table.Value.SetValue(parent, read);
                if (result != null)
                    return result;
            }
            return null;
        }
    }
}
